﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
using FarseerGames.FarseerPhysics.Collisions;


namespace FolderData
{
    /// <summary>
    /// 리소스의 원본을 관리합니다. 
    /// Dictionary로 관리하므로 찾아 쓰는데는 O(1)의 시간이 걸립니다.
    /// </summary>
    public class ResourceManager
    {
        private static ResourceManager _instance;

        public static ResourceManager Instance
        {
            get
            {
                if (_instance == null)
                    throw new Exception();

                return _instance;
            }
        }


        #region Fields
        private Dictionary<string, Character> _characters;
        private Dictionary<string, Terrain> _terrains;
        private Dictionary<string, Item> _items;
        private Dictionary<string, Map> _maps;
        
        private List<string> _characterAssetNames;
        private List<string> _terrainAssetNames;
        private List<string> _itemAssetNames;
        private List<string> _mapAssetNames;


        public List<string> CharacterAssetNames
        {
            get { return _characterAssetNames; }
        }
        public List<string> TerrainAssetNames
        {
            get { return _terrainAssetNames; }
        }
        public List<string> ItemAssetNames
        {
            get { return _itemAssetNames; }
        }
        public List<string> MapAssetNames
        {
            get { return _mapAssetNames; }
        }

        [ContentSerializerIgnore]
        public ICollection<Terrain> Terrains
        {
            get { return _terrains.Values; }
        }
        [ContentSerializerIgnore]
        public ICollection<Character> Characters
        {
            get { return _characters.Values; }
        }
        [ContentSerializerIgnore]
        public ICollection<Item> Items
        {
            get { return _items.Values; }
        }

        #endregion

        /// <summary>
        /// 리소스 매니저가 가지고 있는 리소스의 clone이 아닌
        /// 원본을 반환합니다.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name"></param>
        /// <returns></returns>
        public T PeekResource<T>(string name) where T : class
        {
            if (typeof(Character).IsAssignableFrom(typeof(T)))
            {
                return _characters[name] as T;
            }
            else if (typeof(Terrain).IsAssignableFrom(typeof(T)))
            {
                return _terrains[name] as T;
            }
            else if (typeof(Item).IsAssignableFrom(typeof(T)))
            {
                return _items[name] as T;
            }
            else if (typeof(Map).IsAssignableFrom(typeof(T)))
            {
                return _maps[name] as T;
            }
            else 
                throw new ArgumentException("ResourceManager가 제공하지 않는 type입니다.");
        }

        /// <summary>
        /// ResourceManager에 등록된 리소스를 하나 받습니다. 
        /// </summary>
        /// <typeparam name="T">받을 리소스의 타입</typeparam>
        /// <param name="name">해당 리소스의 이름</param>
        /// <returns>리소스</returns>
        public T GetResource<T>(string name) where T : class
        {
            if (typeof(Character).IsAssignableFrom(typeof(T)))
            {
                return _characters[name].Clone() as T;
            }
            else if (typeof(Terrain).IsAssignableFrom(typeof(T)))
            {
                return _terrains[name].Clone() as T;
            }
            else if (typeof(Item).IsAssignableFrom(typeof(T)))
            {
                return _items[name].Clone() as T;
            }
            else if (typeof(Map).IsAssignableFrom(typeof(T)))
            {
                return _maps[name].Clone() as T;
            }
            else 
                throw new ArgumentException("ResourceManager가 제공하지 않는 type입니다.");
        }

        /// <summary>
        /// 리소스를 등록합니다.
        /// </summary>
        /// <typeparam name="T">등록할 리소스의 타입</typeparam>
        /// <param name="resources">등록할 리소스의 리스트</param>
        public void SetResource<T>(IList<T> resources) where T : class
        {
            if (typeof(Character).IsAssignableFrom(typeof(T)))
            {
                foreach (T c in resources)
                    _characters.Add((c as Character).Name, c as Character);
            }
            else if (typeof(Terrain).IsAssignableFrom(typeof(T)))
            {
                foreach (T t in resources)
                    _terrains.Add((t as Terrain).Name, t as Terrain);
            }
            else if (typeof(Item).IsAssignableFrom(typeof(T)))
            {
                foreach (T i in resources)
                    _items.Add((i as Item).Name, i as Item);
            }
            else if (typeof(Map).IsAssignableFrom(typeof(T)))
            {
                foreach (T m in resources)
                    _maps.Add((m as Map).Name, m as Map);
            }
            else
                throw new ArgumentException("ResourceManager가 제공하지 않는 type입니다.");
        }

        /// <summary>
        /// 리소스를 등록합니다.
        /// </summary>
        /// <typeparam name="T">등록할 리소스의 타입</typeparam>
        /// <param name="resource">등록할 리소스</param>
        public void SetResource<T>(T resource) where T :class
        {
            if (typeof(Character).IsAssignableFrom(typeof(T)))
            {
                _characters.Add((resource as Character).Name, resource as Character);
            }
            else if (typeof(Terrain).IsAssignableFrom(typeof(T)))
            {
                _terrains.Add((resource as Terrain).Name, resource as Terrain);
            }
            else if (typeof(Item).IsAssignableFrom(typeof(T)))
            {
                _items.Add((resource as Item).Name, resource as Item);
            }
            else if (typeof(Map).IsAssignableFrom(typeof(T)))
            {
                _maps.Add((resource as Map).Name, resource as Map);
            }
            else
                throw new ArgumentException("ResourceManager가 제공하지 않는 type입니다.");
        }



        #region Map Binding


        /// <summary>
        /// 리소스매니저가 가지고 있는 데이터를 통해 Map이 가지고 있는 물체 배치 자료를 가지고
        /// 물체를 실제로 Map에 배치합니다.
        /// </summary>
        /// <param name="manager"></param>
        public void BindDatas(Map map)
        {
            if (map.CharacterDatas != null)
            {
                foreach (Character.CharacterDataSet ds in map.CharacterDatas)
                {
                    Character c = GetResource<Character>(ds.Name);
                    c.BindSimulator(map.Simulator);
                    c.SetDatas(ds);
                    map.Substances.Add(ds.id, c);
                }
            }
            if (map.TerrainDatas != null)
            {
                foreach (Terrain.TerrainDataSet ds in map.TerrainDatas)
                {
                    Terrain t = GetResource<Terrain>(ds.Name);
                    t.BindSimulator(map.Simulator);
                    t.SetDatas(ds);
                    map.Substances.Add(ds.id, t);
                }
            }
            if (map.ItemDatas != null)
            {
                foreach (Item.ItemDataSet ds in map.ItemDatas)
                {
                    Item i = GetResource<Item>(ds.Name);
                    i.BindSimulator(map.Simulator);
                    i.SetDatas(ds);

                    map.Substances.Add(ds.id, i);
                }
            }
            if (map.MonsterBlockPositions != null)
            {
                Vertices verts = new Vertices();
                verts.Add(new Vector2(0f, 0f));
                verts.Add(new Vector2(0f, map.MonsterBlockSize.Y - 1f));
                verts.Add(new Vector2(map.MonsterBlockSize.X - 1f, map.MonsterBlockSize.Y - 1f));
                verts.Add(new Vector2(map.MonsterBlockSize.X - 1f, 0f));

                int count = 0;
                foreach (Vector2 v in map.MonsterBlockPositions)
                {
                    Terrain t = new Terrain();
                    t.Verts = verts;
                    t.Mass = 100f; //dummy
                    t.Layer = Substance.LayerOrder.FIRST;
                    t.IsFoldable = true;
                    t.IsFoldForbidden = false;
                    t.IsMovable = false;
                    t.IsSeperatable = false;
                    t.IsUnfoldBehavior = false;
                    t.IsVirtualGeom = false;
                    t.Name = "MonsterBlock";
                    t.Kind = Terrain.TerrainKind.MONSTER_BLOCK;
                    t.BindSimulator(map.Simulator);
                    t.Position = v;

                    map.Substances.Add(t.Name + count++, t);
                }
            }

            BindPlayer(map);
        }

        /// <summary>
        /// 설정된 좌표와 값으로 플레이어를 배치합니다.
        /// 주의!!
        /// 플레이어를 새로 Substance에 추가하므로 플레이어가 여럿이 될 수도 있으니 플레이어가
        /// Map.Substances에 등록이 되어 있지 않은게 확실할 때만 사용해야 합니다
        /// </summary>
        /// <param name="manager"></param>
        public void BindPlayer(Map map)
        {
            map.Player = GetResource<Character>(map.PlayerAssetName);
            map.Player.BindSimulator(map.Simulator);
            map.Player.SetDatas(map.PlayerDataSet);

            map.Player.Body.LinearDragCoefficient = 1.0f;

            map.AddSubstance(map.Player, "player");
        }


        /// <summary>
        /// Map의 물리계를 초기화합니다. (Bind전으로 돌립니다.)
        /// </summary>
        public void UnbindDatas(Map map)
        {
            foreach (var s in map.Substances.Values)
                s.UnbindSimulator();

            map.Substances.Clear();
        }

        /// <summary>
        /// BindDatas와 동일한 기능을 하는 함수. 
        /// MapEditor에서 어떤 Map이든 이 함수를 통해 해당 Map이 가진 DataSet에서 새로 Substance를 
        /// 만들어야 한다. 
        /// Substance -> Substance.DataSet이 나오는 Dictionary가 반환되는데 이를 통해
        /// 클릭->Substance->Sustance.DataSet으로 연동 가능하다.
        /// AdditionalGeomVerts는 일단 고려하지 않도록 한다.
        /// </summary>
        /// <param name="manager"></param>
        /// <returns></returns>
        public Dictionary<Substance, Substance.DataSet> ReloadDatas(Map map)
        {
            UnbindDatas(map);
            Dictionary<Substance, Substance.DataSet> result = new Dictionary<Substance, Substance.DataSet>();
            foreach (Character.CharacterDataSet ds in map.CharacterDatas)
            {
                Character c = this.GetResource<Character>(ds.Name);
                c.BindSimulator(map.Simulator);
                c.SetDatas(ds);
                map.Substances.Add(ds.id, c);
                ds.Tag = c;
                result.Add(c, ds);
            }
            foreach (Item.ItemDataSet ds in map.ItemDatas)
            {
                Item i = this.GetResource<Item>(ds.Name);
                i.BindSimulator(map.Simulator);
                i.SetDatas(ds);
                map.Substances.Add(ds.id, i);
                ds.Tag = i;
                result.Add(i, ds);
            }
            foreach (Terrain.TerrainDataSet ds in map.TerrainDatas)
            {
                Terrain t = this.GetResource<Terrain>(ds.Name);
                t.BindSimulator(map.Simulator);
                t.SetDatas(ds);
                map.Substances.Add(ds.id, t);
                ds.Tag = t;
                result.Add(t, ds);
            }
            //Additional Geom은 일단 고려하지 않는다.
            //Monster Block은 MapEditor에서 직접 그려주므로 Reload하지 않는다.
            map.Simulator.Update(0f);
            return result;
        }

        #endregion


        private ResourceManager()
        {
            _characterAssetNames = new List<string>();
            _itemAssetNames = new List<string>();
            _terrainAssetNames = new List<string>();
            _mapAssetNames = new List<string>();
            _characters = new Dictionary<string, Character>();
            _items = new Dictionary<string, Item>();
            _terrains = new Dictionary<string, Terrain>();
            _maps = new Dictionary<string,Map>();
        }

        /// <summary>
        /// 초기화 함수. ResoueceManager에 있는 Singleton(Instance)를 설정합니다.
        /// </summary>
        /// <param name="content"></param>
        public static void Initialize(ContentManager content)
        {
            _instance = content.Load<ResourceManager>("resourceManager");
        }

        #region Processor
        public class ResourceManagerReader : ContentTypeReader<ResourceManager>
        {
            protected override ResourceManager Read(ContentReader input, ResourceManager existingInstance)
            {
                bool flag = true;
                int count = -1;
                ResourceManager manager = new ResourceManager();
                while (flag)
                {
                    ResourceManagerWriter.ResourceManagerIndicator indi = (ResourceManagerWriter.ResourceManagerIndicator)(input.ReadInt32());
                    switch (indi)
                    {
                        case ResourceManagerWriter.ResourceManagerIndicator.CHARACTER:
                            count = input.ReadInt32();
                            for (int i = 0; i < count; ++i)
                            {
                                Character c = input.ContentManager.Load<Character>(input.ReadString());
                                c.SetNumFrames();
                                manager.SetResource<Character>(c);
                            }
                            break;
                        case ResourceManagerWriter.ResourceManagerIndicator.ITEM:
                            count = input.ReadInt32();
                            for (int i = 0; i < count; ++i)
                                manager.SetResource<Item>(input.ContentManager.Load<Item>(input.ReadString()));
                            break;
                        case ResourceManagerWriter.ResourceManagerIndicator.TERRAIN:
                            count = input.ReadInt32();
                            for (int i = 0; i < count; ++i)
                                manager.SetResource<Terrain>(input.ContentManager.Load<Terrain>(input.ReadString()));
                            break;
                        case ResourceManagerWriter.ResourceManagerIndicator.MAP:
                            count = input.ReadInt32();
                            for (int i = 0; i < count; ++i)
                                manager.SetResource<Map>(input.ContentManager.Load<Map>(input.ReadString()));
                            break;
                        case ResourceManagerWriter.ResourceManagerIndicator.END:
                            flag = false;
                            break;
                        default:
                            throw new Exception(string.Format("예상치 못한 값이 들어왔습니다. 값: {0}", (int)indi));
                    }
                }
                //맵 데이터를 바인딩!
                foreach (Map m in manager._maps.Values)
                    manager.BindDatas(m);
                return manager;
            }
        }

        [ContentTypeWriter]
        public class ResourceManagerWriter : ContentTypeWriter<ResourceManager>
        {
            /// <summary>
            /// 리소스매니저 asset을 write, read할 때 사용된다.
            /// </summary>
            public enum ResourceManagerIndicator
            {
                END         = 0,
                CHARACTER   = 1,
                TERRAIN     = 2,
                ITEM        = 3,
                MAP         = 4
            }

            protected override void Write(ContentWriter output, ResourceManager value)
            {
                output.Write((int)ResourceManagerIndicator.CHARACTER);
                output.Write(value.CharacterAssetNames.Count);
                foreach (string s in value.CharacterAssetNames)
                    output.Write(s);
                output.Write((int)ResourceManagerIndicator.ITEM);
                output.Write(value.ItemAssetNames.Count);
                foreach (string s in value.ItemAssetNames)
                    output.Write(s);
                output.Write((int)ResourceManagerIndicator.TERRAIN);
                output.Write(value.TerrainAssetNames.Count);
                foreach (string s in value.TerrainAssetNames)
                    output.Write(s);
                output.Write((int)ResourceManagerIndicator.MAP);
                output.Write(value.MapAssetNames.Count);
                foreach (string m in value.MapAssetNames)
                    output.Write(m);
                output.Write((int)ResourceManagerIndicator.END);
            }
            public override string GetRuntimeReader(TargetPlatform targetPlatform)
            {
                return typeof(ResourceManager.ResourceManagerReader).AssemblyQualifiedName;
            }
            public override string GetRuntimeType(TargetPlatform targetPlatform)
            {
                return typeof(ResourceManager).AssemblyQualifiedName;
            }
        }

        #endregion
    }
}
